﻿// -----------------------------------------------------------------------
// <copyright file="GroupedCollection.cs" company="">
//      Copyright 2012 Samuel Englard
//      
//      Microsoft Reciprocal License (Ms-RL)
// 
//      This license governs use of the accompanying software. If you use the software, you accept this license. If you do 
//      not accept the license, do not use the software.
// 
//      1. Definitions
// 
//      The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under 
//      U.S. copyright law.
// 
//      A "contribution" is the original software, or any additions or changes to the software.
// 
//      A "contributor" is any person that distributes its contribution under this license.
// 
//      "Licensed patents" are a contributor's patent claims that read directly on its contribution.
// 
//      2. Grant of Rights
// 
//      (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in 
//      section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its 
//      contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works 
//      that you create.
// 
//      (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 
//      3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, 
//      have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or 
//      derivative works of the contribution in the software.
// 
//      3. Conditions and Limitations
// 
//      (A) Reciprocal Grants- For any file you distribute that contains code from the software (in source code or binary 
//      format), you must provide recipients the source code to that file along with a copy of this license, which license 
//      will govern that file. You may license other files that are entirely your own work and do not contain code from the
//      software under any terms you choose.
// 
//      (B) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
// 
//      (C) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, 
//      your patent license from such contributor to the software ends automatically.
// 
//      (D) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution 
//      notices that are present in the software.
// 
//      (E) If you distribute any portion of the software in source code form, you may do so only under this license by 
//      including a complete copy of this license with your distribution. If you distribute any portion of the software in 
//      compiled or object code form, you may only do so under a license that complies with this license.
// 
//      (F) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, 
//      guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot 
//      change. To the extent permitted under your local laws, the contributors exclude the implied warranties of 
//      merchantability, fitness for a particular purpose and non-infringement.
//
// </copyright>
// -----------------------------------------------------------------------

using System.Collections.ObjectModel;
using System.Runtime.Serialization;

namespace System.Collections.Generic
{
    /// <summary>
    ///     A collection that groups results.
    /// </summary>
    public class GroupedCollection<TGroup, TValue> : IEnumerable<KeyValuePair<TGroup, ReadOnlyCollection<TValue>>>, IEnumerable
    {
        /// <summary>
        ///     The actual collection.
        /// </summary>
        private InternalDictionary collection;
        /// <summary>
        ///     The function that decides what group an item goes to.
        /// </summary>
        private GroupDecider<TGroup, TValue> decider;
        /// <summary>
        ///     Number of items in the collection
        /// </summary>
        private int count;

        /// <summary>
        ///     Creates a new collection that groups results.    
        /// </summary>
        /// <param name="decider">
        ///     A function that decides what group an item is in.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        ///     <paramref name="decider"/> cannot be <c>null</c> (<c>Nothing</c> in Visual Basic).
        /// </exception>
        public GroupedCollection(GroupDecider<TGroup, TValue> decider)
        {
            if (decider == null)
            {
                throw new ArgumentNullException("decider", "'decider' cannot be null (Nothing in Visual Basic).");
            }

            count = 0;
            this.decider = decider;
            collection = new InternalDictionary();
        }

        /// <summary>
        ///     Adds an item to the collection.
        /// </summary>
        /// <param name="value">
        ///     The item to add to the collection.
        /// </param>
        /// <returns>
        ///     The group that <paramref name="value"/> was put in.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">
        ///     <paramref name="value"/> cannot be <c>null</c> (<c>Nothing</c> in Visual Basic).
        /// </exception>
        public TGroup Add(TValue value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value", "'value' cannot be null (Nothing in Visual Basic).");
            }

            return AddItem(value);
        }

        /// <summary>
        ///     Adds an item to the collection.
        /// </summary>
        /// <param name="value">
        ///     The item to add to the collection.
        /// </param>
        /// <returns>
        ///     The group that <paramref name="value"/> was put in.
        /// </returns>
        protected virtual TGroup AddItem(TValue item)
        {
            TGroup group = decider.Invoke(item);

            if (!collection.ContainsKey(group))
            {
                collection.Add(group, CreateNewCollection());
            }

            collection[group].Add(item);

            count++;

            return group;
        }

        /// <summary>
        ///     Creates a new collection to use in a group.
        /// </summary>
        /// <returns>
        ///     A new collection of <typeparamref name="TValue"/>
        /// </returns>
        /// <remarks>
        ///     Allows inheritors to have a different type of collection used, as long as it inherits from <see cref="System.Collections.ObjectModel.Collection{T}"/>.
        /// </remarks>
        protected virtual Collection<TValue> CreateNewCollection()
        {
            return new Collection<TValue>();
        }

        /// <summary>
        ///     Removes an item from the collection.
        /// </summary>
        /// <param name="value">
        ///     The item to remove.
        /// </param>
        /// <returns>
        ///     If <paramref name="value"/> was removed returns <c>true</c>, otherwise <c>false</c>.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">
        ///     <paramref name="value"/> cannot be <c>null</c> (<c>Nothing</c> in Visual Basic).
        /// </exception>
        public bool Remove(TValue value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value", "'value' cannot be null (Nothing in Visual Basic).");
            }

            return RemoveItem(value);
        }

        /// <summary>
        ///     Removes an item from the collection.
        /// </summary>
        /// <param name="value">
        ///     The item to remove.
        /// </param>
        /// <returns>
        ///     If <paramref name="value"/> was removed returns <c>true</c>, otherwise <c>false</c>.
        /// </returns>
        protected virtual bool RemoveItem(TValue item)
        {
            TGroup group = decider.Invoke(item);

            if (!collection.ContainsKey(group))
            {
                return false;
            }

            if (collection[group].Remove(item))
            {
                count--;
                return true;
            }
            return false;
        }

        /// <summary>
        ///     The items in a group.
        /// </summary>
        /// <param name="group">
        ///     The group to get items for.
        /// </param>
        /// <returns>
        ///     A collection of items in the group <paramref name="group"/>.
        /// </returns>
        /// <exception cref="System.ArgumentNullException">
        ///     <paramref name="group"/> cannot be <c>null</c> (<c>Nothing</c> in Visual Basic).
        /// </exception>
        /// <remarks>
        ///     If the group does not exist it is created.
        /// </remarks>
        public ReadOnlyCollection<TValue> this[TGroup group]
        {
            get
            {
                if (group == null)
                {
                    throw new ArgumentNullException("group", "'group' cannot be null (Nothing in Visual Basic).");
                }

                if (!collection.ContainsKey(group))
                {
                    collection.Add(group, CreateNewCollection());
                }
                return new ReadOnlyCollection<TValue>(collection[group]);
            }
        }

        /// <summary>
        ///     Clears the collection.
        /// </summary>
        public void Clear()
        {
            ClearItems();
        }

        /// <summary>
        ///     Clears the collection.
        /// </summary>
        protected virtual void ClearItems()
        {
            collection.Clear();
        }

        /// <summary>
        ///     Check if the collection contains an item.
        /// </summary>
        /// <param name="value">
        ///     The item to look for.
        /// </param>
        /// <returns>
        ///     If the collection contains <paramref name="value"/> returns <c>true</c>, otherwise <c>false</c>.
        /// </returns>
        public bool Contains(TValue value)
        {
            TGroup group = decider.Invoke(value);

            if (!collection.ContainsKey(group))
            {
                return false;
            }

            return collection[group].Contains(value);
        }

        /// <summary>
        ///     Check what group an item would be in.
        /// </summary>
        /// <param name="value">
        ///     The item to check.
        /// </param>
        /// <returns>
        ///     The group that <paramref name="value"/> would be in.
        /// </returns>
        public TGroup GroupOf(TValue value)
        {
            return decider.Invoke(value);
        }

        /// <summary>
        ///     The amount of items in the collection.
        /// </summary>
        public int Count
        {
            get
            {
                return count;
            }
        }

        /// <summary>
        ///     Provides access to the inner collection for inheritors.
        /// </summary>
        protected InternalDictionary Collection
        {
            get
            {
                return collection;
            }
        }

        /// <summary>
        ///     The groups in the collection
        /// </summary>
        public Dictionary<TGroup, Collection<TValue>>.KeyCollection Groups
        {
            get
            {
                return collection.Keys;
            }
        }

        #region IEnumerable<KeyValuePair<TGroup,ReadOnlyCollection<TValue>>> Members

        public IEnumerator<KeyValuePair<TGroup, ReadOnlyCollection<TValue>>> GetEnumerator()
        {
            foreach (TGroup key in collection.Keys)
            {
                yield return new KeyValuePair<TGroup, ReadOnlyCollection<TValue>>(key, new ReadOnlyCollection<TValue>(collection[key]));
            }
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            foreach (TGroup key in collection.Keys)
            {
                yield return new KeyValuePair<TGroup, ReadOnlyCollection<TValue>>(key, new ReadOnlyCollection<TValue>(collection[key]));
            }
        }

        #endregion

        /// <summary>
        ///     Class used for the internal collection.
        /// </summary>
#if !WINDOWS_PHONE && !SILVERLIGHT
        [Serializable]
#endif
        protected class InternalDictionary : Dictionary<TGroup, Collection<TValue>>
        {
            /// <summary>
            ///     Initializes a new instance of the <see cref="System.Collections.Generic.GroupedCollection{TGroup,TValue}.InternalDictionary"/> class that is empty, has the default initial capacity, and uses the default equality comparer for the key type.
            /// </summary>
            public InternalDictionary() : base()
            {
            }

            /// <summary>
            ///     Initializes a new instance of the <see cref="System.Collections.Generic.GroupedCollection{TGroup,TValue}.InternalDictionary"/> class with serialized data.
            /// </summary>
            /// <param name="info">
            ///     A <see cref="System.Runtime.Serialization.SerializationInfo"/> object containing the information required to serialize the <see cref="System.Collections.Generic.GroupedCollection{TGroup,TValue}.InternalDictionary"/>.
            /// </param>
            /// <param name="context">
            ///     A <see cref="System.Runtime.Serialization.StreamingContext"/> structure containing the source and destination of the serialized stream associated with the <see cref="System.Collections.Generic.GroupedCollection{TGroup,TValue}.InternalDictionary"/>.
            /// </param>
            protected InternalDictionary(SerializationInfo info, StreamingContext context) : base (info, context)
            {
            }
        }
    }

    /// <summary>
    ///     A method that is used by <see cref="System.Collections.Generic.GroupedCollection{TGroup,TValue}"/> to decide what group an idem should be in.
    /// </summary>
    /// <typeparam name="TGroup">
    ///     The type of the grouping.
    /// </typeparam>
    /// <typeparam name="TValue">
    ///     The type of values that are in the <see cref="System.Collections.Generic.GroupedCollection{TGroup,TValue}"/>.
    /// </typeparam>
    /// <param name="value">
    ///     The value to group.
    /// </param>
    /// <returns>
    ///     The group that <paramref name="value"/> belongs in.
    /// </returns>
    public delegate TGroup GroupDecider<TGroup, TValue>(TValue value);
}
